package nl.utwente.eemcs.decomposition.transformation;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import nl.utwente.eemcs.graph.CommunicatorNode;
import nl.utwente.eemcs.graph.CommunicatorType;
import nl.utwente.eemcs.graph.Edge;
import nl.utwente.eemcs.graph.EdgeType;
import nl.utwente.eemcs.graph.Graph;
import nl.utwente.eemcs.graph.GraphCollaboration;
import nl.utwente.eemcs.graph.Node;
import nl.utwente.eemcs.graph.distribution.DistributionList;
import nl.utwente.eemcs.graph.distribution.DistributionLocation;


public class DataEdgeValidatorVariableSharing {
	private DistributionList distroList;
	private GraphCollaboration collaboration;
	private HashSet<Graph> allGraphs;
	
	private List<Edge> edgesToCreate;
	private List<Edge> edgesToDelete;
	
	private boolean createNewEdges;
	
	private DataEdgeValidatorVariableSharing(GraphCollaboration collaboration, DistributionList distroList, boolean createNewEdges) {
		this.collaboration = collaboration;
		this.distroList = distroList;
		this.allGraphs = new HashSet<Graph>();
		allGraphs.addAll(collaboration.getProcesses());
		
		// Initialize lists
		edgesToCreate = new ArrayList<Edge>();
		edgesToDelete = new ArrayList<Edge>();
		
		this.createNewEdges = createNewEdges; 
	}
	
	public static void analyze(GraphCollaboration collaboration, DistributionList distroList, boolean createNewEdges) {
		DataEdgeValidatorVariableSharing validator = new DataEdgeValidatorVariableSharing(collaboration, distroList, createNewEdges);
		validator.validate();
	}
	
	private void validate() {
		// Get all data edges
		Set<Edge> edges = collaboration.getDataEdges();// collectAllDataEdges();
		
		// Walk through data edges
		for (Edge e : edges) {
			// If the start and end node are the same, data item is created. These edges can be maintained
			if (e.getStart() != e.getEnd()) {
				// Determine the graphs in which the nodes of the edge are defined
				Graph startGraph = nodeInWhichGraph(e.getStart());
				Graph endGraph = nodeInWhichGraph(e.getEnd());
				
				// If the start and end graph are not the same, the data is send via a communicator from one process to another (maybe even via multiple processes)
				if (startGraph != endGraph) {
					List<Node> path = new ArrayList<Node>();
					
					// Search the path to the end node
					if (findNode(e.getEnd(), startGraph, path, new HashSet<Graph>())) {
						
						// Validate the path and check if data restrictions have been violated
						validatePath(e, path);
						
						// Create new data edges
						if (createNewEdges)
							createDataEdges(e, path);
					}
				} else
					validatePath(e, new ArrayList<Node>());
			}
		}
		
		if (createNewEdges) {
			// Remove old depricated data edges
			for (Edge e : edgesToDelete) {
				collaboration.getMainProcess().getEdges().remove(e);
				//collaboration.getInterProcessEdges().remove(e);
			}
			// Create new data edges
			for (Edge e : edgesToCreate) {
				collaboration.getMainProcess().getEdges().add(e);	
				//collaboration.getInterProcessEdges().add(e);
			}
		}
	}
	
	/**
	 * Looks for a path to a certain node
	 * @param nodeToFind The node that should be found
	 * @param currentGraph Current graph that is examined
	 * @param path The path until now
	 * @param visitedGraphs The graphs that were examined
	 * @return True, if the nodeToFind is found
	 */
	private boolean findNode(Node nodeToFind, Graph currentGraph, List<Node> path, HashSet<Graph> visitedGraphs) {
		if (visitedGraphs.contains(currentGraph))
			return false;
		visitedGraphs.add(currentGraph);
		
		// If the node was found, return true
		if (currentGraph.getNodes().contains(nodeToFind))
			return true;
		
		// Look for communicators and follow
		HashSet<CommunicatorNode> comNodes = getCommunicators(currentGraph, CommunicatorType.InvokeRec, CommunicatorType.Response);
		for (CommunicatorNode n : comNodes) {
			CommunicatorNode correspondant = getMatchingCommunicator(n, collaboration.getCommunicationEdges());
			Graph correspondantGraph = nodeInWhichGraph(correspondant);
			
			// Copy path
			List<Node> newPath = new ArrayList<Node>();
			for (Node np : path)
				newPath.add(np);
			newPath.add(n);
			newPath.add(correspondant);
			
			// Search along the path
			if (findNode(nodeToFind, correspondantGraph, newPath, visitedGraphs)) {
				// Copy path and return
				path.clear();
				for (Node np : newPath)
					path.add(np);
				return true;
			}
		}		
		
		return false;
	}
	
	/**
	 * Compare the nodes on the path with the distribution location of the data items
	 * @param originalEdge The original edge that is currently processed
	 * @param path The path found from the startNode to the endNode of the edge
	 */
	private void validatePath(Edge originalEdge, List<Node> path) {
		// Get the data item name
		String dataItem = originalEdge.getLabel();
		
		// Check if there is a data restriction defined for the current data item
		if (distroList.getDataItemRestrictions().containsKey(dataItem)) {
			// Distribution location of the data item (restriction)
			DistributionLocation distrLoc = distroList.getDataItemRestrictions().get(dataItem);
			
			// Check start and end node
			if (originalEdge.getStart().getLocation() != distrLoc)
				collaboration.addDataRestrictionError(dataItem, originalEdge.getStart());
			if (originalEdge.getEnd().getLocation() != distrLoc)
				collaboration.addDataRestrictionError(dataItem, originalEdge.getEnd());			
				
			// Check nodes along the path
			for (Node n : path)
				if (n.getLocation() != distrLoc)
					collaboration.addDataRestrictionError(dataItem, n);
		}
	}
	
	/**
	 * Collects all the data edges that were defined
	 * @return List with all the data edges
	 */
	@SuppressWarnings("unused")
   private HashSet<Edge> collectAllDataEdges() {
		HashSet<Edge> edges = new HashSet<Edge>();
		edges.addAll(getDataEdges(collaboration.getMainProcess()));
		for (Graph g : collaboration.getProcesses())
			edges.addAll(getDataEdges(g));
		return edges;
	}
	
	/**
	 * Get the data edges within a graph
	 * @param g Graph
	 * @return List with data edges
	 */
	private List<Edge> getDataEdges(Graph g) {
		List<Edge> result = new ArrayList<Edge>();
		for (Graph subGraph : g.getAllGraphsAndSubGraphs())
			result.addAll(subGraph.getEdges(EdgeType.Data));
		return result;
	}
	
	/**
	 * Find out in which graph the node is defined
	 * @param n Node
	 * @return The graph in which the node is used
	 */
	private Graph nodeInWhichGraph(Node n) {
		for (Graph mainGraph : allGraphs) {
			for (Graph subGraph : mainGraph.getAllGraphsAndSubGraphs())
				if (subGraph.getNodes().contains(n))
					return mainGraph;
		}
		return null;
	}
	
	/**
	 * Get the communicator nodes within a certain graph
	 * @param process Graph
	 * @return
	 */
	private HashSet<CommunicatorNode> getCommunicators(Graph process) {
		HashSet<CommunicatorNode> result = new HashSet<CommunicatorNode>();

		for (Graph subGraphs : process.getAllGraphsAndSubGraphs())
			for (Node n : subGraphs.getNodesByType(CommunicatorNode.class))
				result.add((CommunicatorNode)n);
			
		
		return result;
	}
	
	/**
	 * Get communicators within a certain graph with a CommunicatorType
	 * @param process Graph
	 * @param type CommunicatorType
	 * @return
	 */
	private HashSet<CommunicatorNode> getCommunicators(Graph process, CommunicatorType... type) {
		HashSet<CommunicatorNode> result = new HashSet<CommunicatorNode>();
		for (CommunicatorNode n : getCommunicators(process))
			for (CommunicatorType t : type)
				if (t == n.getType())
					result.add(n);
		return result;
	}
	
	/**
	 * Get the communicator that communicates with a particular communicator
	 * @param node Communicator
	 * @param communicationEdges The communication edges list
	 * @return
	 */
	private CommunicatorNode getMatchingCommunicator(CommunicatorNode node, Set<Edge> communicationEdges) {
		for (Edge e : communicationEdges) {
			if (e.getEdgeType() == EdgeType.Communication && e.getStart() == node)
				return (CommunicatorNode)e.getEnd();
			else if (e.getEdgeType() == EdgeType.Communication && e.getEnd() == node)
				return (CommunicatorNode)e.getStart();
		}
		return null;
	}
	
	/**
	 * Define edges that should be created along a path and mark old data edge for deletion
	 * @param dataEdge Data edge
	 * @param path The path
	 */
	private void createDataEdges(Edge dataEdge, List<Node> path) {
		Node currentStart = dataEdge.getStart();
		if (path.size() > 0) {
			for (Node n : path) {
				if (n instanceof CommunicatorNode && ( ((CommunicatorNode) n).getType() == CommunicatorType.Response || ((CommunicatorNode) n).getType() == CommunicatorType.InvokeRec) && !edgeDoesExist(currentStart, n, dataEdge))
					edgesToCreate.add(createDataEdge(currentStart, n, dataEdge));
				currentStart = n;
			}
			edgesToCreate.add(createDataEdge(currentStart, dataEdge.getEnd(), dataEdge));
			edgesToDelete.add(dataEdge);
		}
	}
	
	private Edge createDataEdge(Node from, Node to, Edge oldEdge) {
		Edge newEdge = new Edge(from, to, EdgeType.Data);
		newEdge.setLabel(oldEdge.getLabel());
		for (String key : oldEdge.getAttributes().keySet())
			newEdge.addAttribute(key, oldEdge.getAttribute(key));			
		return newEdge;
	}
	
	/**
	 * Check if a new data edge is necessary or if it is already present
	 * @param from From
	 * @param to To
	 * @param oldEdge The old edge
	 * @return
	 */
	private boolean edgeDoesExist(Node from, Node to, Edge oldEdge) {
		for (Edge e : edgesToCreate) {
			if (e.getStart() == from && e.getEnd() == to && e.getLabel().equals(oldEdge.getLabel()))
				return true;
		}
		return false;
	}
}
